package club.gskj.gulimall.product.service.impl;

import club.gskj.gulimall.product.service.CategoryBrandRelationService;
import club.gskj.gulimall.product.vo.CatelogSecondVo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import club.gskj.common.utils.PageUtils;
import club.gskj.common.utils.Query;

import club.gskj.gulimall.product.dao.CategoryDao;
import club.gskj.gulimall.product.entity.CategoryEntity;
import club.gskj.gulimall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    private CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );
        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        //  1.查询所有分类
        List<CategoryEntity> categoryEntities = baseMapper.selectList(null);

        //  2.封装树形
        //  获取所有的 一级分类list
        List<CategoryEntity> levelMenu1 = categoryEntities.stream().filter(category -> category.getParentCid() == 0)
                .map((menu) -> {
                    menu.setChildren(getChildrens(menu, categoryEntities));
                    return menu;
                })
                .sorted((menu1, menu2) -> {
                    return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
                })
                .collect(Collectors.toList());

        return levelMenu1;
    }


    @Override
    public void removeMenuByIds(List<Long> asList) {
        //TODO  检查菜单是否被其他引用.
        // 采用逻辑删除
        baseMapper.deleteBatchIds(asList);
    }


    // [1,12,225]
    @Override
    public Long[] getCateLogPath(Long catelogId) {
        List<Long> longs = new ArrayList<>();

        List<Long> parentPath = findParentPath(catelogId, longs);
        // 逆叙
        Collections.reverse(parentPath);
        return parentPath.toArray(new Long[parentPath.size()]);
    }

    /**
     * 级联更新
     *
     * @CacheEvict: 失效模式, 当缓存中对应的数据库数据被修改后，清除缓存中的旧数据，下次查询先访问数据库，加过再存入缓存中。避免缓存脏数据产生
     * <p>
     * 1. 同时进行多种缓存操作 @Caching：如果需要同时删除缓存区中保存的多条数据，则使用  @Caching执行多条缓存注解。
     * 2. 指定删除某个分区下的所有数据，@CacheEvict(value = "category",allEntries = true)
     * 3. 规范：存储同一类型的数据，都可以指定为同一个分区，分区名默认为缓存的前缀
     */
    /*
    @CacheEvict(value = "category", key = "#root.methodName")
    @Caching(evict = {
            @CacheEvict(value = "category", key = "'ListLevelOne'"),
            @CacheEvict(value = "category", key = "'getCatelogJson'")
    })
    */
    //@CachePut// 双写模式： 将修改过后的最新数据，向缓存中再放一份
    @CacheEvict(value = "category", allEntries = true) // 失效模式、清除模式
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        if (!StringUtils.isEmpty(category.getName())) {
            // TODO 级联更新，其他表中关联字段
            categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
        }
    }

    /**
     * 1. 每一个需要缓存的数据，都指定要放到那个缓存区,(按照业务类型区分)
     * 2. @Cacheable:当前方法的返回结果，需要缓存。如果缓存中有，该方法不调用。没有则调用。并将结果返回缓存.
     * 3. 默认行为
     * 3.1 缓存中有结果，该方法不调用
     * 3.2 key默认自动生成 缓存名称::SimpleKey [](自动生成的key值)
     * 3.3 缓存的value值。默认使用JDK序列化机制
     * 3.4 默认时间-1,永不过期
     * 4.自定义
     * 4.1. 指定生成的缓存key:  key属性指定，接受SpEl表达式
     * 4.2. 指定缓存的过期时间: 在application.properties配置文件中修改ttl值
     * 4.3. 指定保存value值为JSON格式
     * 自定义RedisCahceConfiguration。设置key,value的序列化器
     * <p>
     * 5.spring Cache 的不足
     * 5.1 读模式：
     * 缓存穿透：查询一个null数据， 解决：缓存空数据 spring.cache.redis.cache-null-values=true
     * 缓存击穿：大量并发进来时，同时查询一个刚好过期的数据。 解决： 加锁？ sync = true(加锁，解决击穿：本地锁)
     * 缓存雪崩：大量key同时过期，解决：加随机时间，加上过期时间：spring.cache.redis.time-to-live=3600000
     * 5.2 写模式：
     * 读写加锁
     * 引入canal,感知到mysql的数据更新去更新缓存
     * 读多写多，直接查询数据库，不做缓存操作
     * 总结：
     * 常规数据：(读多写少，即时性，一致性要求不高的数据) 可以使用 SpringCache; 写模式(缓存的数据设置过期时间既可以满足)
     * 特殊数据：必须特殊对待
     */
    @Cacheable(value = {"category"}, key = "#root.methodName", sync = true)
    @Override
    public List<CategoryEntity> ListLevelOne() {
        //System.out.println("ListLevelOne...");
        return this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
    }

    /**
     * TODO 在做(apache-jmeter-5.4.1)压力测试时，会出现此异常 OutOfDirectMemoryError 堆外内存溢出。
     * 1. springBoot2.x 默认使用lettuce操作redis的客户端，使用netty进行网络通信。
     * 2. lettuce的Bug导致netty的内存溢出 ,项目VM虚拟机设置的内存为 -Xmx300m； 如果没有为netty设置堆外内存，默认会使用 -Xmx300m;
     * 可以通过 -Dio.netty.maxDirectMenory进行设置.
     * <p>
     * 解决方案：不能使用  -Dio.netty.maxDirectMenory 去调堆外内存。
     * 1. 升级 lettuce客户端
     * 2. 切换使用jedis客户端操作redis
     * redisTemplate:
     * lettuce、jedis操作redis底层客户端，spring再次封装为redisTemplate
     */
    //@Override
    public Map<String, List<CatelogSecondVo>> getCatelogJson2() {
        // 加入缓存,(缓存中存储的数据必须是 json),通过序列化、反序列化来获取真正的对象
        /**
         *  1. 空结果缓存：解决缓存穿透
         *  2.设置过期时间(加随机值),解决缓存雪崩
         *  3.加锁：解决缓存击穿
         */
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        String catelogJSON = ops.get("catelogJSON");
        // 查询缓存中是否有此数据
        if (StringUtils.isEmpty(catelogJSON)) {
            // 没有数据、直接查询数据库、并放入缓存
            Map<String, List<CatelogSecondVo>> catelogJsonFromDb = getCatelogJsonFromDbWithRedisLock();
            return catelogJsonFromDb;
        }
        // 处理字符串
        Map<String, List<CatelogSecondVo>> stringListMap = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<CatelogSecondVo>>>() {
        });
        return stringListMap;
    }

    /**
     * 使用SpringCache 再次封装,将查询到的数据放入 缓存
     *
     * @return
     */
    @Cacheable(value = "category", key = "#root.methodName")
    @Override
    public Map<String, List<CatelogSecondVo>> getCatelogJson() {
        List<CategoryEntity> categoryEntityList = this.baseMapper.selectList(null);

        // 查询所有1级分类
        List<CategoryEntity> categoryEntitiesOne = getParent_cid(categoryEntityList, 0L);

        // 封装数据
        Map<String, List<CatelogSecondVo>> stringListMap = categoryEntitiesOne.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            // 拿到每一个一级分类，查询该一级分类下的二级分类
            List<CategoryEntity> categoryEntities = getParent_cid(categoryEntityList, v.getCatId());
            // 封装指定格式
            List<CatelogSecondVo> collect = null;
            if (categoryEntities != null) {
                collect = categoryEntities.stream().map(item -> {
                    // 二级分类VO
                    CatelogSecondVo catelogSecondVo = new CatelogSecondVo(v.getCatId().toString(), null, item.getCatId().toString(), item.getName());
                    // 查询当前二级分类的三级分类集合
                    List<CategoryEntity> categoryEntities3 = getParent_cid(categoryEntityList, item.getCatId());

                    if (categoryEntities3 != null) {
                        List<CatelogSecondVo.catelogThirdVo> collect3 = categoryEntities3.stream().map(item3 -> {
                            // 封装三级分类 Vo
                            CatelogSecondVo.catelogThirdVo catelogThirdVo = new CatelogSecondVo.catelogThirdVo(item.getCatId().toString(), item3.getCatId().toString(), item3.getName());
                            return catelogThirdVo;
                        }).collect(Collectors.toList());
                        catelogSecondVo.setCatalog3List(collect3);
                    }
                    return catelogSecondVo;
                }).collect(Collectors.toList());
            }
            return collect;
        }));
        return stringListMap;
    }

    /**
     * 使用分布式 redisson 锁实现，
     * 缓存里面的数据如何跟数据库保持一致？
     * 缓存数据一致性
     * 1. 双写模式
     * 2. 失效模式
     * <p>
     * 最终解决方案
     * 1. 缓存的所有数据都设有过期时间，数据过期自动触发查询更新加缓存操作
     * 2. 读写数据的时候，加上分布式的读写锁，
     * (只有写的时候才会有等待,读的时候是共享读)
     */
    public Map<String, List<CatelogSecondVo>> getCatelogJsonFromDbWithRedissonLock() {

        // 1. 锁的名称一样，获取的锁就一样。 锁的粒度，越细越快
        // 锁的粒度，具体缓存的式某个数据： 11号商品，product-11-lock
        RLock lock = redissonClient.getLock("catelogJson-lock");
        // 加锁
        lock.lock();
        Map<String, List<CatelogSecondVo>> dataFromDb = null;
        try {
            // 无论是否发生异常,都要删除锁
            dataFromDb = getDataFromDb();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 解锁
            lock.unlock();
        }
        return dataFromDb;
    }

    /**
     * 使用 reids 分布式锁
     *
     * @return
     */
    public Map<String, List<CatelogSecondVo>> getCatelogJsonFromDbWithRedisLock() {
        // UUID 标识当前线程所添加的锁
        String uuid = UUID.randomUUID().toString();

        // 占分布式锁, 并设置过期时间: 原子性操作
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);

        // 抢占成功，返回ture，抢占失败返回false
        if (lock) {
            System.out.println("获取分布式锁成功.....");
            // 加锁成功，执行业务
            // 设置过期时间30s，防止服务发送异常没法删除该锁，出现 "死锁"状态
            // 这种先占锁，再设置过期时间有异常风险，因为占锁、设置过期时间必须是一个原子性操作，在占锁的同时设置上过期时间才可以保证服务的稳定性，即避免 "死锁"的发生。
            //stringRedisTemplate.expire("lock", 30, TimeUnit.MINUTES);

            Map<String, List<CatelogSecondVo>> dataFromDb = null;
            try {
                // 无论是否发生异常,都要删除锁
                dataFromDb = getDataFromDb();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 释放锁，删除锁也会有与占锁的相同问题，必须保证删除锁是原子性操作。
                //stringRedisTemplate.delete("lock");

                // 使用lua脚本解锁,删除锁。保存操作的原子性
                //"if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                //        "then\n" +
                //        "    return redis.call(\"del\",KEYS[1])\n" +
                //        "else\n" +
                //        "    return 0\n" +
                //        "end";
                String script = "if redis.call('get',KEYS[1] == ARGV[1] then return redis.call('del',KEYS[1])) else return 0 end;";
                // 原子性删除锁
                // 每次删除当前线程锁时，根据当前方法中的uuid,判断redis中的保存锁的value是否与uuid一致，一致则删除当前线程所保存的锁。避免因为删除锁的时序问题：当前线程删除了其他线程向redis保存的锁。
                // lock1 删除锁成功返回 1 ,删除失败返回 0
                Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock", uuid));
            }
            return dataFromDb;
            /**
             *  reids分布式锁的核心:
             *      加锁必须是原子性
             *      删除锁必须是原子性
             */
        } else {
            System.out.println("获取分布式锁失败.....自旋重试获取锁");
            try {
                // 加锁失败，重试等待2s，继续占锁
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 通过自旋的方式，加锁失败等待后，重新加锁
            return getCatelogJsonFromDbWithRedisLock();
        }
    }

    /**
     * 从数据库中查询三级分离。并封装为指定格式。再放入缓存中
     *
     * @return
     */
    private Map<String, List<CatelogSecondVo>> getDataFromDb() {
        // 先查询缓存，缓存有直接取，不再查询数据库
        String catelogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
        if (!StringUtils.isEmpty(catelogJSON)) {
            // 缓存中不为null, 直接返回
            Map<String, List<CatelogSecondVo>> stringListMap = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<CatelogSecondVo>>>() {
            });
            return stringListMap;
        }
        System.out.println("查询了数据库------------->");
        /**
         * TODO 性能优化、 数据库多次查询优化 一次查询
         * 查询所有分类列表
         */
        List<CategoryEntity> categoryEntityList = this.baseMapper.selectList(null);

        // 查询所有1级分类
        List<CategoryEntity> categoryEntitiesOne = getParent_cid(categoryEntityList, 0L);

        // 封装数据
        Map<String, List<CatelogSecondVo>> stringListMap = categoryEntitiesOne.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            // 拿到每一个一级分类，查询该一级分类下的二级分类
            List<CategoryEntity> categoryEntities = getParent_cid(categoryEntityList, v.getCatId());
            // 封装指定格式
            List<CatelogSecondVo> collect = null;
            if (categoryEntities != null) {
                collect = categoryEntities.stream().map(item -> {
                    // 二级分类VO
                    CatelogSecondVo catelogSecondVo = new CatelogSecondVo(v.getCatId().toString(), null, item.getCatId().toString(), item.getName());
                    // 查询当前二级分类的三级分类集合
                    List<CategoryEntity> categoryEntities3 = getParent_cid(categoryEntityList, item.getCatId());

                    if (categoryEntities3 != null) {
                        List<CatelogSecondVo.catelogThirdVo> collect3 = categoryEntities3.stream().map(item3 -> {
                            // 封装三级分类 Vo
                            CatelogSecondVo.catelogThirdVo catelogThirdVo = new CatelogSecondVo.catelogThirdVo(item.getCatId().toString(), item3.getCatId().toString(), item3.getName());
                            return catelogThirdVo;
                        }).collect(Collectors.toList());
                        catelogSecondVo.setCatalog3List(collect3);
                    }
                    return catelogSecondVo;
                }).collect(Collectors.toList());
            }
            return collect;
        }));
        // 将查询的数据转为 JSON字符串放入缓存中 (确保查询数据、放入缓存是原子性操作，必须保证是在同一把锁中进行的！)
        String s = JSON.toJSONString(stringListMap);
        // 存入缓存指定过期时间
        stringRedisTemplate.opsForValue().set("catelogJSON", s, 1, TimeUnit.DAYS);
        return stringListMap;
    }

    /**
     * 从数据库中 查询
     * 使用本地锁，
     */
    public Map<String, List<CatelogSecondVo>> getCatelogJsonFromDbWithLocalLock() {

        /**
         *  加锁： 只要是同一把锁，就可以锁住需要这把锁的所有线程
         *   synchronized (this):springboot容器中的所有对象都是单例的
         *   TODO 本地锁 synchronized, JUC(Lock),智能锁住当前服务的进程。在分布式情况下，想要锁住所有服务的所有进程，必须使用分布式锁。
         */
        synchronized (this) {
            // 先查询缓存，缓存有直接取，不再查询数据库
            ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
            String catelogJSON = ops.get("catelogJSON");
            if (!StringUtils.isEmpty(catelogJSON)) {
                // 缓存中不为null, 直接返回
                Map<String, List<CatelogSecondVo>> stringListMap = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<CatelogSecondVo>>>() {
                });
                return stringListMap;
            }
            System.out.println("查询了数据库------------->");
            /**
             * TODO 性能优化、 数据库多次查询优化 一次查询
             * 查询所有分类列表
             */
            List<CategoryEntity> categoryEntityList = this.baseMapper.selectList(null);

            // 查询所有1级分类
            List<CategoryEntity> categoryEntitiesOne = getParent_cid(categoryEntityList, 0L);

            // 封装数据
            Map<String, List<CatelogSecondVo>> stringListMap = categoryEntitiesOne.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
                // 拿到每一个一级分类，查询该一级分类下的二级分类
                List<CategoryEntity> categoryEntities = getParent_cid(categoryEntityList, v.getCatId());
                // 封装指定格式
                List<CatelogSecondVo> collect = null;
                if (categoryEntities != null) {
                    collect = categoryEntities.stream().map(item -> {
                        // 二级分类VO
                        CatelogSecondVo catelogSecondVo = new CatelogSecondVo(v.getCatId().toString(), null, item.getCatId().toString(), item.getName());
                        // 查询当前二级分类的三级分类集合
                        List<CategoryEntity> categoryEntities3 = getParent_cid(categoryEntityList, item.getCatId());

                        if (categoryEntities3 != null) {
                            List<CatelogSecondVo.catelogThirdVo> collect3 = categoryEntities3.stream().map(item3 -> {
                                // 封装三级分类 Vo
                                CatelogSecondVo.catelogThirdVo catelogThirdVo = new CatelogSecondVo.catelogThirdVo(item.getCatId().toString(), item3.getCatId().toString(), item3.getName());
                                return catelogThirdVo;
                            }).collect(Collectors.toList());
                            catelogSecondVo.setCatalog3List(collect3);
                        }
                        return catelogSecondVo;
                    }).collect(Collectors.toList());
                }
                return collect;
            }));
            // 将查询的数据转为 JSON字符串放入缓存中 (确保查询数据、放入缓存是原子性操作，必须保证是在同一把锁中进行的！)
            String s = JSON.toJSONString(stringListMap);
            // 存入缓存指定过期时间
            ops.set("catelogJSON", s, 1, TimeUnit.DAYS);
            return stringListMap;
        }
    }

    /**
     * 获取该一级分类下对应的所有二级分类-二级分类下的三级分类....
     */
    private List<CategoryEntity> getParent_cid(List<CategoryEntity> categoryEntityList, Long parent_id) {
        //return this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
        return categoryEntityList.stream().filter(item -> item.getParentCid() == parent_id).collect(Collectors.toList());
    }

    /***
     *  递归查询 当前category上的所有父级id
     * @param catelogId
     * @param longs
     * @return
     * // 225，25,12
     */
    public List<Long> findParentPath(Long catelogId, List<Long> longs) {
        // 收集当前id
        longs.add(catelogId);
        CategoryEntity byId = this.getById(catelogId);
        if (byId.getParentCid() != 0) {
            findParentPath(byId.getParentCid(), longs);
        }
        return longs;
    }

    /***
     *  查询当前root 下的所有子菜单list
     * @param root 父菜单
     * @param all  所有菜单
     */
    public static List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {

        List<CategoryEntity> childrent = all.stream().filter(categoryEntity -> {
            // 查询子菜单
            return categoryEntity.getParentCid() == root.getCatId();
        })
                .map(categoryEntity -> {
                    // 递归查询子菜单
                    categoryEntity.setChildren(getChildrens(categoryEntity, all));
                    return categoryEntity;
                })
                .sorted((menu1, menu2) -> {
                    // 菜单排序
                    return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
                })
                .collect(Collectors.toList());
        return childrent;
    }
}
